/**
 * Copyright 2014-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.certapp.service;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.certapp.common.code.ConstantCode;
import com.certapp.common.code.RetCode;
import com.certapp.common.entity.BaseResponse;
import com.certapp.common.enums.AccountStatus;
import com.certapp.common.exception.BaseException;
import com.certapp.common.properties.ConstantProperties;
import com.certapp.common.utils.JacksonUtils;
import com.certapp.dao.entity.TbAccountInfo;
import com.certapp.dao.entity.TbAccountInfoExample;
import com.certapp.dao.mapper.TbAccountInfoMapper;
import com.certapp.entity.AccountInfo;
import com.certapp.entity.LoginInfo;
import lombok.extern.log4j.Log4j2;

/**
 * services for account data.
 */
@Log4j2
@Service
public class AccountService {

    @Autowired
    private TbAccountInfoMapper tbAccountMapper;
    @Qualifier(value = "bCryptPasswordEncoder")
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private TokenService tokenService;
    @Autowired
    private ConstantProperties constants;

    public static final String TOKEN_HEADER_NAME = "AuthorizationToken";
    public static final String HEADER_ACCOUNT = "Account";
    public static final String TOKEN_START = "Token";
    public static final String ADMIN_TOKEN_VALUE = "admin";

    /**
     * login.
     */
    public TbAccountInfo login(LoginInfo loginInfo) throws BaseException {
        log.info("start login. loginInfo:{}", JacksonUtils.toJSONString(loginInfo));
        String accountStr = loginInfo.getAccount();
        String passwordStr = loginInfo.getAccountPwd();

        // check account
        accountExist(accountStr);

        // check pwd
        if (StringUtils.isBlank(passwordStr)) {
            log.info("fail login. passwordStr is null");
            throw new BaseException(ConstantCode.PASSWORD_ERROR);
        }
        // encode by bCryptPasswordEncoder
        TbAccountInfoExample example = new TbAccountInfoExample();
        example.createCriteria().andAccountEqualTo(accountStr);
        TbAccountInfo accountRow = tbAccountMapper.getOneByExample(example).get();
        if (!passwordEncoder.matches(passwordStr, accountRow.getAccountPwd())) {
            // reset login fail time
            int loginFailTime = accountRow.getLoginFailTime() + 1;
            log.info("fail login. pwd error,loginFailTime:{}", loginFailTime);
            accountRow.setLoginFailTime(loginFailTime);
            accountRow.setModifyTime(LocalDateTime.now());
            tbAccountMapper.updateByPrimaryKeySelective(accountRow);
            throw new BaseException(ConstantCode.PASSWORD_ERROR);
        }

        return accountRow;
    }

    /**
     * update account info.
     */
    public void updateAccountRow(String currentAccount, AccountInfo accountInfo)
            throws BaseException {
        String accountStr = accountInfo.getAccount();
        // check account
        accountExist(accountStr);

        // query by account
        TbAccountInfoExample example = new TbAccountInfoExample();
        example.createCriteria().andAccountEqualTo(accountStr);
        TbAccountInfo accountRow = tbAccountMapper.getOneByExample(example).get();

        // encode password
        if (StringUtils.isNoneBlank(accountInfo.getAccountPwd())) {
            String encryptStr = passwordEncoder.encode(accountInfo.getAccountPwd());
            accountRow.setAccountPwd(encryptStr);
            // the current user is admin
            if (!currentAccount.equals(accountStr)) {
                accountRow.setAccountStatus(AccountStatus.UNMODIFIEDPWD.getValue());
            } else {
                accountRow.setAccountStatus(AccountStatus.NORMAL.getValue());
            }
        }

        // update account info
        accountRow.setModifyTime(LocalDateTime.now());
        Integer affectRow = tbAccountMapper.updateByPrimaryKeySelective(accountRow);
        log.debug("end updateAccountRow. affectRow:{}", affectRow);
    }

    /**
     * update password.
     */
    public void updatePassword(String targetAccount, String oldAccountPwd, String newAccountPwd)
            throws BaseException {
        log.debug("start updatePassword. targetAccount:{} oldAccountPwd:{} newAccountPwd:{}",
                targetAccount, oldAccountPwd, newAccountPwd);

        // query target account info
        TbAccountInfoExample example = new TbAccountInfoExample();
        example.createCriteria().andAccountEqualTo(targetAccount);
        Optional<TbAccountInfo> record = tbAccountMapper.getOneByExample(example);
        TbAccountInfo targetRow = record.isPresent() ? record.get() : null;
        if (targetRow == null) {
            log.warn("fail updatePassword. not found target row. targetAccount:{}", targetAccount);
            throw new BaseException(ConstantCode.ACCOUNT_NOT_EXISTS);
        }

        if (StringUtils.equals(oldAccountPwd, newAccountPwd)) {
            log.warn("fail updatePassword. the new pwd cannot be same as old ");
            throw new BaseException(ConstantCode.NEW_PWD_EQUALS_OLD);
        }

        // check old password
        if (!passwordEncoder.matches(oldAccountPwd, targetRow.getAccountPwd())) {
            throw new BaseException(ConstantCode.PASSWORD_ERROR);
        }

        // update password
        targetRow.setAccountPwd(passwordEncoder.encode(newAccountPwd));
        targetRow.setAccountStatus(AccountStatus.NORMAL.getValue());
        targetRow.setModifyTime(LocalDateTime.now());
        Integer affectRow = tbAccountMapper.updateByPrimaryKeySelective(targetRow);

        log.debug("end updatePassword. affectRow:{}", affectRow);

    }

    /**
     * query account info by accountName.
     */
    public TbAccountInfo queryByAccount(String accountStr) {
        log.debug("start queryByAccount. accountStr:{} ", accountStr);
        TbAccountInfoExample example = new TbAccountInfoExample();
        example.createCriteria().andAccountEqualTo(accountStr);
        Optional<TbAccountInfo> accountRow = tbAccountMapper.getOneByExample(example);
        log.debug("end queryByAccount. accountRow:{} ", JacksonUtils.toJSONString(accountRow));
        return accountRow.isPresent() ? accountRow.get() : null;
    }

    /**
     * query count of account.
     */
    public int countOfAccount(String account) {
        TbAccountInfoExample example = new TbAccountInfoExample();
        example.createCriteria().andAccountEqualTo(account);
        Integer accountCount = (int) tbAccountMapper.countByExample(example);
        int count = accountCount == null ? 0 : accountCount.intValue();
        return count;
    }

    /**
     * get current account. whether use security:
     * 
     * @case1: get account info from request's header, such as account of admin001
     * @case2: get account from token, use token to get account, such as 0x001 to get its account of
     *         admin001
     */
    public String getCurrentAccount(HttpServletRequest request) {
        if (!constants.getIsUseSecurity()) {
            String accountDefault = getAccount(request);
            if (StringUtils.isNotBlank(accountDefault)) {
                log.debug("getCurrentAccount default [{}]", accountDefault);
                return accountDefault;
            } else {
                // default return admin
                log.debug("getCurrentAccount not found account, default admin");
                return ADMIN_TOKEN_VALUE;
            }
        }
        String token = getToken(request);
        return tokenService.getValueFromToken(token);
    }

    /**
     * boolean account is exist.
     */
    public void accountExist(String account) throws BaseException {
        if (StringUtils.isBlank(account)) {
            throw new BaseException(ConstantCode.ACCOUNT_NAME_EMPTY);
        }
        int count = countOfAccount(account);
        if (count == 0) {
            throw new BaseException(ConstantCode.ACCOUNT_NOT_EXISTS);
        }
    }

    /**
     * boolean account is not exit.
     */
    public void accountNotExist(String account) throws BaseException {
        if (StringUtils.isBlank(account)) {
            throw new BaseException(ConstantCode.ACCOUNT_NAME_EMPTY);
        }
        int count = countOfAccount(account);
        if (count > 0) {
            throw new BaseException(ConstantCode.ACCOUNT_EXISTS);
        }
    }

    /**
     * get token.
     */
    public static synchronized String getToken(HttpServletRequest request) {
        String header = request.getHeader(TOKEN_HEADER_NAME);
        if (StringUtils.isBlank(header)) {
            log.error("not found token");
            throw new BaseException(ConstantCode.INVALID_TOKEN);
        }

        String token = StringUtils.removeStart(header, TOKEN_START).trim();
        if (StringUtils.isBlank(token)) {
            log.error("token is empty");
            throw new BaseException(ConstantCode.INVALID_TOKEN);
        }
        return token;
    }

    /**
     * get token.
     */
    public static synchronized String getAccount(HttpServletRequest request) {
        String accountName = request.getHeader(HEADER_ACCOUNT);
        log.debug("getAccount from header: [{}]", accountName);
        return accountName;
    }

    public static void responseString(HttpServletResponse response, String str) {
        log.info("responseString:{}", str);
        BaseResponse baseResponse = new BaseResponse(ConstantCode.SYSTEM_EXCEPTION);
        if (StringUtils.isNotBlank(str)) {
            baseResponse.setMsg(str);
        }
        if (StringUtils.isNotBlank(str) && str.equals(ConstantCode.INVALID_TOKEN.getMsg())) {
            baseResponse = new BaseResponse(ConstantCode.INVALID_TOKEN);
        }
        if (StringUtils.isNotBlank(str) && str.equals(ConstantCode.TOKEN_EXPIRE.getMsg())) {
            baseResponse = new BaseResponse(ConstantCode.TOKEN_EXPIRE);
        }

        RetCode retCode;
        if (JacksonUtils.isJson(str)
                && (retCode = JacksonUtils.toJavaObject(str, RetCode.class)) != null) {
            baseResponse = new BaseResponse(retCode);
        }

        try {
            response.getWriter().write(JacksonUtils.toJSONString(baseResponse));
        } catch (IOException e) {
            log.error("fail responseRetCodeException", e);
        }
    }
}
